package dnl.jexem.cmd;

import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

/**
 * Base class for all executions that wrap a {@code java.lang.Process}.
 *
 * @author Daniel Orr
 *
 */
public abstract class BaseLocalCommandExecution extends CommandExecution {
    
    protected long startTime = -1;
    protected long endTime = -1;
    protected Process process;
    
    public BaseLocalCommandExecution(String... command) {
        super(command);
    }
    
    public BaseLocalCommandExecution(Command command) {
        super(command);
    }

    /**
     * Gets the time this process was started at.
     *
     * @return time in milliseconds. If -1 this means this execution was not started.
     */
    public long getStartTime() {
        return startTime;
    }
    
    @Override
    public void executeCommand(long timeout, boolean block) {
        try {
            prepareProcess();
        } catch (IOException ex) {
            //Logger.getLogger(BaseLocalCommandExecution.class.getName()).log(Level.SEVERE, null, ex);
            exceptionMessages.add(ex.getMessage());
            this.exitStatus = 1;
            setExecutionStatus(ExecutionStatus.CANNOT_EXECUTE);
            return;
        }
        
        try (InputStream inputStream = process.getInputStream();
                InputStream errorStream = process.getErrorStream()) {
            handleProcessIO(inputStream, errorStream, block);
            handleProcessTermination();
            
        } catch (IOException | InterruptedException e) {
            exceptionMessages.add(e.getMessage());
            setExecutionStatus(ExecutionStatus.UNCERTAIN);
        }
    }
    
    protected abstract void handleProcessIO(InputStream inputStream, InputStream errorStream, boolean block) throws IOException, InterruptedException;

    /**
     * Resolves execution statuses after the process has terminated.
     *
     * @throws java.lang.InterruptedException
     */
    protected void handleProcessTermination() throws InterruptedException {
        process.waitFor();
        this.exitStatus = process.exitValue();
        TerminationReason tr = exitStatus == 0
                ? TerminationReason.COMPLETED_SUCCESSFULY
                : TerminationReason.ABNORMAL_TERMINATION;
        
        markStatuses(ExecutionStatus.TERMINATED, tr);
    }

    /**
     * Prepares and starts the underlying Process.
     *
     * @throws IOException
     */
    protected void prepareProcess() throws IOException {
        setExecutionStatus(ExecutionStatus.PRE_RUN);
        List<String> commandAndArgs = new ArrayList<>();
        commandAndArgs.add(command.getCommand());
        commandAndArgs.addAll(Arrays.asList(command.getArgs()));
        ProcessBuilder pb = new ProcessBuilder(commandAndArgs);
        if (getWorkDir() != null) {
            File workDirFile = new File(getWorkDir());
            if (!workDirFile.exists() || !workDirFile.isDirectory()) {
                markStatuses(ExecutionStatus.CANNOT_EXECUTE, TerminationReason.WORK_DIR_DOES_NOT_EXIST);
                return;
            }
            pb.directory(workDirFile);
        }
        this.process = pb.start();
        setExecutionStatus(ExecutionStatus.RUNNING);
        this.startTime = System.currentTimeMillis();
    }
    
    @Override
    public void kill() {
        try {
            if (process != null) {
                process.destroy();
            }
        } finally {
            markStatuses(ExecutionStatus.TERMINATED, TerminationReason.KILLED);
        }
    }
    
    protected void killOnTimeout() {
        try {
            if (process != null) {
                process.destroy();
            }
        } finally {
            markStatuses(ExecutionStatus.TERMINATED, TerminationReason.KILLED_AFTER_TIMEOUT);
        }
    }
    
    protected void markStatuses(ExecutionStatus executionStatus, TerminationReason terminationReason) {
        setExecutionStatus(executionStatus);
        this.terminationReason = terminationReason;
        if (ExecutionStatus.TERMINATED.equals(executionStatus)) {
            this.endTime = System.currentTimeMillis();
        }
    }
    
    @Override
    public String getAllOutputs() {
        StringBuilder sb = new StringBuilder();
        sb.append("Output:\n");
        sb.append(getOutput());
        sb.append("Error:\n");
        sb.append(getErrorOutput());
        sb.append("Exceptions:\n");
        for (String ex : exceptionMessages) {
            sb.append("_____________________________");
            sb.append(ex);
        }
        return sb.toString();
    }
    
}
